/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.domain;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xptracker.persistence.Identified;
import org.xptracker.persistence.PersistenceManager;
import org.xptracker.util.CollectionUtils;
import org.xptracker.util.ThreadLocalCleaner;

import java.text.MessageFormat;
import java.util.*;

/**
 * This object is responsible for querying for items in the application domain.
 */
@SuppressWarnings({"serial"})
@Singleton
public class Domain {
  private final Map<Class, EntityFinder> ENTITY_FINDERS = new HashMap<Class, EntityFinder>() {{
    put(Story.class, new EntityFinder() {
      public Identified find(String id) {
        return Domain.this.loadStory(new Long(id));
      }
    });
  }};

  private final Map<Class, PersistenceManager.FetchHelper> FETCH_HELPERS = new HashMap<Class, PersistenceManager.FetchHelper>() {{
    put(Story.class, STORY_FETCH_HELPER);
    put(User.class, USER_FETCH_HELPER);
  }};

  private static final PersistenceManager.FetchHelper<Story> STORY_FETCH_HELPER = new PersistenceManager.FetchHelper<Story>() {
    public void fetch(Story story) {
      story.getTrackers().size();
      story.getFileAttachments().size();
      story.getTasks().size();
      story.getCustomers().size();
      story.getBlogEntries().size();
    }
  };

  private static final PersistenceManager.FetchHelper<User> USER_FETCH_HELPER = new PersistenceManager.FetchHelper<User>() {
    public void fetch(User user) {
      user.getHomepageQueries().size();
      user.getQueries().size();
      user.getQueues().size();
      user.getTasks().size();
      user.getTimelines().size();
      user.getBookmarkedStories().size();
    }
  };
  
  private static final Map<Class, String> ENTITY_LOADER_QUERIES = new HashMap<Class, String>() {{
    put(Queue.class, LOAD_QUEUE_BY_ID);
    put(Timeline.class, LOAD_TIMELINE_BY_ID);
  }};

  private static final String LOAD_USER_QUERY = "User.load";
  private static final String LIST_ENABLED_USERS_QUERY = "User.list";
  private static final String LIST_ALL_USERS_QUERY = "User.listAll";
  private static final String LIST_ENTITIES = "Identified.list";
  private static final String LIST_STORY_STATES = "StoryState.list";
  private static final String LIST_STORY_SNAPSHOTS = "StoryDescriptionSnapshot.list";
  private static final String LOAD_STORY_BY_EXTID = "Story.loadByExtId";
  private static final String LIST_STORIES_BY_OWNER_QUERY = "Story.listByOwnerAndState";
  private static final String LIST_STORIES_BY_TASK_OWNER_QUERY = "Story.listByTaskOwnerAndState";
  private static final String LIST_STORIES_BY_CATEGORY_QUERY = "Story.listByCategory";
  private static final String LIST_STORIES_BY_CUSTOMER_QUERY = "Story.listByCustomer";
  private static final String LIST_EFFORT_ENTRIES = "EffortEntry.listByStoryAndDate";
  private static final String LIST_BY_STATE = "Story.listByState";
  private static final String LIST_BLOG_ENTRIES_AFTER = "BlogEntry.listAfter";
  private static final String LOAD_CATEGORY_QUERY = "StoryCategory.load";
  private static final String LOAD_CUSTOMER_BY_NAME_QUERY = "Customer.loadByName";
  private static final String LIST_OPEN_QUEUES = "Queue.listOpen";
  private static final String LIST_VISIBLE_QUEUES = "Queue.listVisible";
  private static final String LOAD_QUEUE_BY_NAME = "Queue.loadByName";
  private static final String LOAD_QUEUE_BY_ID = "Queue.loadById";
  private static final String LOAD_WIKI_PAGE = "WikiPage.load";

  private static final String NAMED_SET = "NamedSet.load";
  private static final String NAMED_MAP = "NamedMap.load";

  private static final String LOAD_TIMELINE = "Timeline.load";
  private static final String LOAD_TIMELINE_BY_ID = "Timeline.loadById";

  private PersistenceManager persistenceManager;

  private ResourceBundle queries;

  private Log log = LogFactory.getLog(getClass());
  private static ThreadLocal<StoryPointMapping> mapping =
      ThreadLocalCleaner.registerForCleanup(new ThreadLocal<StoryPointMapping>());

  @Inject
  public Domain(PersistenceManager mgr) {
    this.persistenceManager = mgr;
  }

  public <T> T loadEntity(Class<? extends T> entityType, String uuid) {
    if (ENTITY_LOADER_QUERIES.containsKey(entityType)) {
      //noinspection unchecked
      return (T) querySingle(ENTITY_LOADER_QUERIES.get(entityType), toMap("id", uuid), entityType.getSimpleName());
    }
    return uuid == null ? null : persistenceManager.loadObject(entityType, uuid);
  }

  public <T> T querySingleReplace(String query, Object... args) {
    List<T> results = queryReplace(query, args);
    int size = results.size();
    if (size >= 1) {
      if (size == 1) {
        return results.get(0);
      } else {
        throw new MultipleResultsException(query, args, size);
      }
    }
    return null;
  }

  public <T> T querySingle(String query, Map<String, Object> args, Object... replacements) {
    List<T> results = query(query, args, replacements);
    int size = results.size();
    if (size >= 1) {
      if (size == 1) {
        return results.get(0);
      } else {
        throw new MultipleResultsException(query, args, size);
      }
    }
    return null;
  }

  public <T> List<T> queryReplace(String query, Object... args) {
    List<T> results = persistenceManager.query(loadQuery(query, args));
    if (log.isDebugEnabled()) {
      log.debug("Results: " + results);
    }
    return results;
  }

  public <T> List<T> query(String query, Map<String, Object> args, Object... replacements) {
    List<T> results = persistenceManager.query(loadQuery(query, replacements), args);
    if (log.isDebugEnabled()) {
      log.debug("Results: " + results);
    }
    return results;
  }

  public <T> List<T> query(String query, Comparator<T> comparator, Map<String, Object> args, Object... replacements) {
    List<T> list = query(query, args, replacements);
    Collections.sort(list, comparator);
    return list;
  }

  public <T> List<T> queryReplace(String query, Comparator<T> comparator, Object... args) {
    List<T> list = queryReplace(query, args);
    Collections.sort(list, comparator);
    return list;
  }

  private String loadQuery(String key, Object... parameters) {
    String query = getQueries().getString(key);
    if (StringUtils.isNotBlank(query)) {
      String formattedQuery = MessageFormat.format(query, cleanup(parameters));
      log.debug("Query: " + formattedQuery);
      return formattedQuery;
    }
    throw new RuntimeException("Query not found by key '" + key + "'");
  }

  private static Object[] cleanup(Object[] parameters) {
    int len = parameters.length;
    Object[] newParams = new Object[len];
    for (int i = 0; i < len; i++) {
      Object o = parameters[i];
      if (o instanceof String) {
        newParams[i] = String.valueOf(o).replaceAll("\"", "\\\"");
      } else {
        newParams[i] = o;
      }
    }
    return newParams;
  }

  public User loadUser(String username) {
    return querySingle(LOAD_USER_QUERY, toMap("username", username));
  }

  /**
   * List all ENABLED users.
   *
   * @return all enabled users.
   */
  public List<User> listUsers() {
    return listUsers(false);
  }

  @SuppressWarnings({"unchecked"})
  public List<User> listUsers(boolean includeDisabled) {
    return queryReplace(includeDisabled ? LIST_ALL_USERS_QUERY : LIST_ENABLED_USERS_QUERY, new BeanComparator("username"));
  }

  @SuppressWarnings({"unchecked"})
  public List<StoryCategory> listCategories() {
    return queryReplace(LIST_ENTITIES, new BeanComparator("name"), StoryCategory.class.getSimpleName());
  }

  public StoryCategory loadCategory(String name) {
    return querySingleReplace(LOAD_CATEGORY_QUERY, name);
  }

  public List<Story> listStoriesByOwner(UserAccessor user) {
    Set<Story> stories = new HashSet<Story>();
    addStoriesByQueryAndState(stories, LIST_STORIES_BY_OWNER_QUERY, State.inProcess, user);
    addStoriesByQueryAndState(stories, LIST_STORIES_BY_OWNER_QUERY, State.blocked, user);
    addStoriesByQueryAndState(stories, LIST_STORIES_BY_TASK_OWNER_QUERY, State.inProcess, user);
    addStoriesByQueryAndState(stories, LIST_STORIES_BY_TASK_OWNER_QUERY, State.blocked, user);

    List<Story> finalList = new ArrayList<Story>(stories);
    //noinspection unchecked
    Collections.sort(finalList, Collections.reverseOrder(new BeanComparator("lastModified")));
    return finalList;
  }

  private void addStoriesByQueryAndState(Collection<Story> stories, String query, State state, UserAccessor user) {
    List<Story> localStories = query(query, toMap("state", state.toString(), "username", user.getUsername()));
    stories.addAll(localStories);
  }

  private Map<String, Object> toMap(Object... values) {
    Map<String, Object> map = new HashMap<String, Object>();
    int len = values.length;
    if (len % 2 == 0) {
      for (int i = 0; i < len; i += 2) {
        map.put(String.valueOf(values[i]), values[i + 1]);
      }
    } else {
      throw new IllegalArgumentException("Length of value array must be even.");
    }
    return map;
  }

  public List<Story> listStoriesByState(State state) {
    return query(LIST_BY_STATE, toMap("state", state.toString()));
  }

  @SuppressWarnings({"unchecked"})
  public List<Story> listStories() {
    return queryReplace(LIST_ENTITIES, new BeanComparator("id"), Story.class.getSimpleName());
  }

  public List<Story> listStoriesByCategory(StoryCategory category) {
    return query(LIST_STORIES_BY_CATEGORY_QUERY, toMap("categoryId", category.getId()));
  }

  @SuppressWarnings({"unchecked"})
  public List<BlogEntry> listBlogEntriesAfter(Date date, UserAccessor user) {
    return query(LIST_BLOG_ENTRIES_AFTER, Collections.reverseOrder(new BeanComparator("createdDate")),
        toMap("created", date, "username", user.getUsername()));
  }

  public List<Task> listCompletedTasksForUser(User user, boolean isCompleted) {
    List<Task> completedTasks = new ArrayList<Task>();
    for (Task task : user.getTasks()) {
      if (task.isComplete() == isCompleted) {
        completedTasks.add(task);
      }
    }
    return completedTasks;
  }

  @SuppressWarnings({"unchecked"})
  public List<Queue> listQueues(boolean showHidden) {
    if (!showHidden) {
      return queryReplace(LIST_VISIBLE_QUEUES);
    }
    return queryReplace(LIST_ENTITIES, new BeanComparator("name"), Queue.class.getSimpleName());
  }

  @SuppressWarnings({"unchecked"})
  public List<Queue> listOpenQueues() {
    return queryReplace(LIST_OPEN_QUEUES, new BeanComparator("name"));
  }

  public List<ScheduledTask> listScheduledTasks() {
    return queryReplace(LIST_ENTITIES, ScheduledTask.class.getSimpleName());
  }

  public Queue loadQueueByName(String queueName) {
    return querySingleReplace(LOAD_QUEUE_BY_NAME, queueName);
  }

  @SuppressWarnings({"unchecked"})
  public List<Customer> listCustomers() {
    return queryReplace(LIST_ENTITIES, new BeanComparator("name"), Customer.class.getSimpleName());
  }

  public EmailConfiguration loadEmailConfiguration() {
    return querySingleReplace(LIST_ENTITIES, EmailConfiguration.class.getSimpleName());
  }

  public StoryPointMapping loadPointMapping(boolean useCache) {
    StoryPointMapping mapping = Domain.mapping.get();
    if (!useCache || mapping == null) {
      mapping = (StoryPointMapping) querySingleReplace(LIST_ENTITIES, StoryPointMapping.class.getSimpleName());
      if (mapping == null) {
        mapping = new StoryPointMapping();
      }
      Domain.mapping.set(mapping);
    }
    return mapping;
  }

  public int getPoints(Story story) {
    return loadPointMapping(true).getMap().get(story.getSize().toString());
  }

  public NamedSet getNamedSet(PersistenceManager.UpdateReceiver receiver, String name) {
    NamedSet set = querySingle(NAMED_SET, toMap("name", name));
    if (set == null) {
      set = new NamedSet(name);
      receiver.insert(set);
    }
    return set;
  }

  public NamedMap getNamedMap(PersistenceManager.UpdateReceiver receiver, String name) {
    NamedMap map = querySingle(NAMED_MAP, toMap("name", name));
    if (map == null) {
      map = new NamedMap(name);
      receiver.insert(map);
    }
    return map;
  }

  public Customer loadCustomer(String name) {
    return querySingle(LOAD_CUSTOMER_BY_NAME_QUERY, toMap("name", name));
  }

  public List<Story> listStoriesByCustomer(Customer customer) {
    return query(LIST_STORIES_BY_CUSTOMER_QUERY, toMap("customerId", customer.getId()));
  }

  public List<Timeline> listTimelines() {
    List<Timeline> timelines = queryReplace(LIST_ENTITIES, Timeline.class.getSimpleName());
    //noinspection unchecked
    Collections.sort(timelines, new BeanComparator("name"));
    return timelines;
  }

  public Timeline loadTimeline(String name) {
    return querySingle(LOAD_TIMELINE, toMap("name", name));
  }

  public static void setStoryPointMapping(StoryPointMapping localMapping) {
    mapping.set(localMapping);
  }

  public Story loadStory(Long extId) {
    return querySingle(LOAD_STORY_BY_EXTID, toMap("extId", extId));
  }

  public List<Story> loadStories(Long... ids) {
    return loadItems("jsq/listStoriesByExtIds.js", ids);
  }

  public List<WikiPage> loadWikiPages(Long... ids) {
    return loadItems("jsq/listWikiPagesByIds.js", ids);
  }

  private <T> List<T> loadItems(String query, Long... ids) {
    if (ids.length > 0) {
      List<T> list = persistenceManager.queryWithScript(query, ids, "ignored");
      return new ArrayList<T>(new LinkedHashSet<T>(list));
    }
    return Collections.emptyList();
  }

  /**
   * Find an item that might be one of the given types.  This method will loop over each of the given classes
   * and, if an item is found of that type with the given ID, it will be returned.  Otherwise, null.
   *
   * @param classes a list of types to search
   * @param id      the ID of the item you're looking for
   * @return the item found, or null
   */
  public Identified findEntity(Set<Class<? extends Identified>> classes, String id) {
    for (Class<? extends Identified> c : classes) {
      Identified result = getEntityFinder(c).find(id);
      if (result != null) {
        return result;
      }
    }
    return null;
  }

  private EntityFinder getEntityFinder(final Class<? extends Identified> c) {
    if (!ENTITY_FINDERS.containsKey(c)) {
      ENTITY_FINDERS.put(c, new EntityFinder() {
        public Identified find(String id) {
          return Domain.this.loadEntity(c, id);
        }
      });
    }
    return ENTITY_FINDERS.get(c);
  }

  public <T extends Identified> T fetchEntity(Class<T> entityType, String id) {
    //noinspection unchecked
    PersistenceManager.FetchHelper<T> helper = FETCH_HELPERS.get(entityType);
    return persistenceManager.loadObject(entityType, id, helper);
  }

  public Story fetchStory(String id) {
    return fetchEntity(Story.class, id);
  }

  public User fetchUser(String id) {
    return fetchEntity(User.class, id);
  }

  public void storeStoryState(final Story story, final User currentUser) {
    persistenceManager.inTransaction(new PersistenceManager.UpdateHandler<Object>() {
      public Object doUpdate(PersistenceManager.UpdateReceiver receiver) {
        StoryState state = new StoryState(story);

        persistenceManager.flush();
        List<StoryState> states = listStoryStates(story.getId());
        Collections.sort(states, new Comparator<StoryState>() {
          public int compare(StoryState first, StoryState second) {
            Date firstDate = new Date(first.getStartDate().getTime());
            Date secondDate = new Date(second.getStartDate().getTime());
            return firstDate.compareTo(secondDate);
          }
        });

        if (states.size() > 0) {
          //Sometimes 2 changes occur at the same time and the last one isn't really the "last" one...
          for (int i = states.size() - 1; i >= 0; i--) {
            StoryState previousState = states.get(i);
            if (previousState.getStopDate() == null) {
              previousState.setStopDate(state.getStartDate());
              receiver.update(previousState);
              break;
            }
          }
        }

        List<StoryDescriptionSnapshot> snapshots = listStorySnapshots(story.getId());
        Collections.sort(snapshots, new Comparator<StoryDescriptionSnapshot>() {
          public int compare(StoryDescriptionSnapshot first, StoryDescriptionSnapshot second) {
            Date firstDate = first.getOccurred();
            Date secondDate = second.getOccurred();
            return secondDate.compareTo(firstDate);
          }
        });

        boolean createSnapshot = true;
        boolean equalTime = false;
        StoryDescriptionSnapshot newSnapshot = new StoryDescriptionSnapshot(story, currentUser);
        if (snapshots.size() > 0) {
          StoryDescriptionSnapshot priorSnapshot = snapshots.get(0);
          if (StringUtils.equals(story.getSummary(), priorSnapshot.getSummary()) &&
              StringUtils.equals(story.getDescription(), priorSnapshot.getDescription())) {
            createSnapshot = false;
          }
          equalTime = newSnapshot.getOccurred().equals(priorSnapshot.getOccurred());
        }

        if (createSnapshot) {
          if (equalTime) {
            newSnapshot.setOccurred(new Date(newSnapshot.getOccurred().getTime() + 1));  // avoids time conflicts
          }
          receiver.insert(newSnapshot);
        }
        receiver.insert(state);
        return null;
      }
    });
  }

  public List<StoryState> listStoryStates(String storyId) {
    return query(LIST_STORY_STATES, toMap("storyId", storyId));
  }

  public List<StoryState> listStoryStates(Date startDate, Date stopDate) {
    List<StoryState> list = persistenceManager.queryWithScript("jsq/listStoryStatesInDateRange.js", startDate, stopDate);
    return new ArrayList<StoryState>(new LinkedHashSet<StoryState>(list));
  }

  public List<StoryDescriptionSnapshot> listStorySnapshots(String storyId) {
    return query(LIST_STORY_SNAPSHOTS, toMap("storyId", storyId));
  }
  
  private ResourceBundle getQueries() {
    if (queries == null) {
      String dbType = persistenceManager.getDbType();
      try {
        queries = ResourceBundle.getBundle("domainQueries-" + dbType);
      } catch (MissingResourceException e) {
        log.warn("No queries found for db type '" + dbType + "'");
      }
    }
    return queries;
  }

  @SuppressWarnings({"unchecked"})
  public List<EffortEntry> listEffortEntries(Story story, Date startDate, Date stopDate) {
    return query(LIST_EFFORT_ENTRIES, new BeanComparator("start"), toMap("storyId", story.getId(), "start", startDate,
        "end", stopDate));
  }

  public List<User> loadUsers(List<String> userNames) {
     return CollectionUtils.map(userNames, new CollectionUtils.Producer<String, User>() {
      public User produce(String from) {
        return loadUser(from);
      }
    });
  }

  public WikiPage loadWikiPage(PersistenceManager.UpdateReceiver receiver, String name) {
    WikiPage page = querySingle(LOAD_WIKI_PAGE, toMap("name", name));
    if (page == null) {
      page = new WikiPage(name, "");
      receiver.insert(page);
    }
    return page;
  }

  public boolean wikiPageExists(String name) {
    return querySingle(LOAD_WIKI_PAGE, toMap("name", name)) != null;
  }

  public List<WikiPage> listWikiPages() {
    return queryReplace(LIST_ENTITIES, WikiPage.class.getSimpleName());
  }

  private interface EntityFinder {
    Identified find(String id);
  }
}
